<?php
/**
* object-oriented file system manipulation as nodes.
* This file belongs to a webappkit package (kit)
* @link http://webappkit.net
* @package pathnodes
* @subpackage common
* @author J.Ducastel <jeremie@ducastel.name>
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*/

/**
* registers as low-level pathnode subclass
*/
pathnode::heritage('filenode',2);
/**
* generic file node.
*/
class fileNode extends pathNode {

	/**
	* detects if path applies to self class
	* @param string $path
	* @static
	* @access public
	* @return bool
	*/
	function selfDetector($path) {
		return is_file($path);
	}/**/
	
	/**
	* read some bytes.
	* Reads all content if bytes is not provided. From offset, if provided, else current offset.
	* @param int bytes optional 1mo by default
	* 1048576 = 1Mo
	* @param int offset optional uses current offset by default
	* @access public
	* @return mixed file content
	*/
	function read($bytes=1048576, $offset=NULL) {
		/*if (!$this->isReadable()) {
			trigger_error('filenode::read : file'.$this->path.' is not readable',E_USER_NOTICE);
			return false;
		}*/
		// getting handle
		if (!$handle=&$this->handle()) {
			trigger_error('filenode::read could not read ('.$this->path.')');
			return false;
		}
		// setting offset if necessary
		$this->offset($offset);
		return fread($handle,$bytes);
	}/**/
	
	/**
	* write some data.
	* From offset, if provided, else current offset.
	* @param mixed $content
	* @param int offset optional. uses current offset if omitted
	* @access public
	* @return bool
	*/
	function write($content, $offset=NULL) {
		// is file writable
		/*if (!$this->isWritable()) {
			trigger_error('filenode::write : file '.$this->path.' is not writable',E_USER_NOTICE);
			return false;
		}*/
		// getting handle
		if (!$handle=&$this->handle())
			return false;
		// setting offset if necessary
		$this->offset($offset);
		return fwrite($handle,$content);
	}/**/
	
	/**
	* erase from offset
	* @param int offset
	* @return bool
	*/
	function erase($offset=NULL) {
		// getting handle
		if (!$handle=&$this->handle())
			return false;
		// setting offset if necessary
		//$oldOffset=$this->offset();
		$from=$this->offset($offset);
		// echo $from;
		$ok=ftruncate($handle, $from);
		// echo $this->offset();
		// $this->offset($offset);
		return $ok;
	}/**/
	
	/**
	* returns and edventually sets handle’s offset.
	* @param int offset optional sets new offset
	* @access public
	* @return int current offset
	*/
	function offset($offset=NULL,$whence=NULL) {
		// getting file handle
		if (!$handle=&$this->handle())
			return false;
		// is new offset provided
		if (is_int($offset)) {
			// setting whence 
			switch ($whence) {
				case SEEK_CUR: // new offset is from current position
				case SEEK_END: // new offset is from the end
					break;
				default: // new offset is from start
					$whence=SEEK_SET;
			}
			// trying to set new offset
			fseek($handle,$offset,$whence);
		}
		return ftell($handle);
	}/**/
	
	/**
	* sets offset to end of file
	* @access public
	* @return int new offset
	*/
	function toEnd() {
		return $this->offset(0,SEEK_END);
	}
	/**
	* sets offset to start of file
	* @access public
	* @return int new offsset
	*/
	function toStart() {
		return $this->offset(0,SEEK_SET);
	}
	
	/**
	* destroy file
	* @access public
	* @return bool
	*/
	function delete() {
		if (!$this->exists) return true;
		$this->exists=!unlink($this->path);
		return !$this->exists;
	}/**/
	
	/**
	* moves file
	* @param string $path
	* @access public
	* @return bool
	* @todo
	* /
	function move($path) {
		
	}/**/
	
	/**
	* get file size in octets
	* @access public
	* @return int
	*/
	function size() {
		return $this->exists()?filesize($this->path):0;
	}/**/
	
	/**
	* returns md5 hash of the file's content
	* @access public
	* @return string
	*/
	function md5() {
		return md5_file($this->path);
	}/**/
	
	/**
	* is the file readable ?
	* @access public
	* @return bool
	*/
	function isReadable() {
		if (PHP_OS=='WINNT')
			return true;
		//clearstatcache();
		return is_readable($this->path);
	}/**/
	
	/**
	* tells if the file can be written
	* @access public
	* @return bool
	*/
	function isWritable() {
		if (PHP_OS=='WINNT')
			return true;
		return is_writable($this->path);
	}/**/

	/**
	 * sends content to browser
	 * @access public
	 * @return bool
	 */
	function printContent() {
		// reading and sending content
		while ($str=$this->read()) {
			print $str;
		}
		return true;
	}/**/
	
	/**
	 * force download
	 * @access public
	 * @return bool
	 */
	function download($filename=null) {
		// read access check
		if (!$this->isReadable()) {
			trigger_error("No read access for file to download {$this->path}",E_USER_WARNING);
			return false;
		}
		// sending headers
		if (!$filename)
			$filename=$this->getBaseName();
		$this->_downloadHeaders($filename);
		// sending content
		return $this->printContent();
	}/**/
/*------------------------------------------------------------------------------
                                                                      properties
------------------------------------------------------------------------------*/
	/**
	* @var type desc
	*/
	/**
	* @var resource file's handle
	*/
	var $_handle;
	/**
	* @var string handle's mode
	*/
	var $mode;
	/**
	* @var int handler position, in octets since start
	*/
	var $_offset=0;
	
	/**
	* returns file handle
	* @access public
	* @param string $mode
	* @return resource file hande
	*/
	function & handle($mode=null) {
		if (!$this->_handle) {
			// defining default mode if required
			if (is_null($mode)) {
				switch (true) {
					case $this->exists() and $this->isWritable():
						$mode='r+';
						break;
					case $this->exists():
						$mode='r';
						break;
					default:
						$mode='w+';
				}
			}
			// opening in requested mode
			if ($this->_handle=fopen($this->path,$mode)) {
				$this->mode=$mode;
				// trigger_error('opened '.$this->path.' in mode '.$mode);
			} else
				trigger_error('failed to open '.$this->path.' in mode '.$mode);
			// echo wakapi::vardump($this);
		}
		return $this->_handle;
	}/**/
	
	/**
	* executes a regex search on self name, returns results array
	* @param string $regex
	* @access public
	* @return array
	*/
	function regexSelfName($regex) {
		if (!ereg($regex,$this->basename,$results))
			return false;
		return $results;
	}/**/
	
	/* force download file to client
	* /
	function sendFile($file, $path=NULL,$name=NULL) {
		// test de lecture

		// envoi des en-tetes
		$size=filesize($path.$file);
		if ($name===NULL) $name=$file;
		$this->sendHeaders($name,$size);

		// ouverture du fichier
		if (!$fd= fopen($path.$file, 'rb')) {
			trigger_error("pas d'acces en lecture sur $path$file",E_USER_WARNING);
			return false;
			}
		//fpassthru($fd);
		// cette methode de decoupage en parties permet de traiter de gros fichiers
		while(!feof($fd)) {
			$buffer = fread($fd, 2048);
			print $buffer;
		}
		fclose ($fd);
		return true;
	}

	/**
	 * sends download headers
	 * @access protected
	 * @param string $filename
	 * @param int $size file size in octets
	 * @return bool
	 */
	function _downloadHeaders($filename=null) {
		// is Internet Explorer ?
		if (isset($_SERVER['HTTP_USER_AGENT']) and $_SERVER['HTTP_USER_AGENT']=="MSIE")
			header("Content-type: application/force-download");
		// or a good browser ?
		else
			header("Content-type: application/octet-stream");
		// if size provided, send size header
		if ($size=$this->size())
			header("Content-Length: ".$size);
		// send filename header
		if (!$filename)
			$filename=$this->getbasename();
		header("Content-Disposition: attachment; filename=".$filename);
		return true;
	}/**/
}
?>